<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
    <script src="https://unpkg.co/gsap@3/dist/gsap.min.js"></script>
    <script>
    </script>

    <style>
      body {
        overflow: hidden;
        padding: 0;
        margin: 0;
      }
      canvas {
        display: block;
      }
      #zoom-range {
        position: absolute;
        width: 200px;
        bottom: 0;
        right: 10px;
        padding: 30px 0;
      }
      .cr.color .selector {
        width: 50px !important;
      }
      .cr.color .saturation-field,
      .cr.color .field-knob {
        display: none !important;
      }
      .cr.color .hue-field {
        width: calc(100% - 8px) !important;
      }
      .cr.color .hue-knob {
        margin-left: 26px !important;
        height: 5px !important;
        border-right: 10px solid #000 !important;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <input type="range" min="0" max="0" value="0" step="1" id="zoom-range" />
    </div>

    <script type="x-shader/x-fragment" id="fragmentShader">
      precision highp float;

      varying vec2 vUv;
      uniform float shrink;
      uniform vec3 base_color_1;
      uniform vec3 base_color_2;
      uniform vec3 mid_color;
      uniform float vignette;
      uniform float brightness;
      uniform float darkness;

      float random (in vec2 st) {
          return fract(sin(dot(st.xy,vec2(12.9898,78.233)))*43758.5453123);
      }

      // Based on Morgan McGuire @morgan3d
      // https://www.shadertoy.com/view/4dS3Wd
      float noise (in vec2 st) {
          vec2 i = floor(st);
          vec2 f = fract(st);
          float a = random(i);
          float b = random(i + vec2(1.0, 0.0));
          float c = random(i + vec2(0.0, 1.0));
          float d = random(i + vec2(1.0, 1.0));
          vec2 u = f * f * (3.0 - 2.0 * f);
          return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
      }

      float fbm (in vec2 st) {
          float value = 0.0;
          float amplitude = .5;
          float frequency = 0.;
          for (int i = 0; i < 4; i++) {
              value += amplitude * noise(st);
              st *= 2.;
              amplitude *= .5;
          }
          return value;
      }

      float length2( vec2 p ) {
          vec2 q = p*p*p*p;
          return pow( q.x + q.y, 1.0/4.0 );
      }

      void main() {

          vec2 st = vec2(.5) - vUv;
          st *= 1.3;
          st.x *= 6.28318531; // 2Pi
          st.y *= 3.14159265359; // Pi

          float r = length(st);
          float a = atan(st.y, st.x);

          float pulsing = 1. + clamp(1. - r, 0., 1.) * shrink;

          // noisy fullscreen mix of 2 colors
          float f = fbm(5. * st);
          vec3 col = mix(base_color_1, base_color_2, f);

          // blury spot in the middle
          col = mix(col, mid_color, 1. - smoothstep(0.2, 0.6, r * (.2 + .8 * pulsing)));

          // white streaks
          a += .05 * fbm(20. * st) * fbm(20. * st);
          f = smoothstep((1. - brightness), 1., fbm(vec2(20. * a, 6. * r * pulsing)));
          col = mix(col, vec3(1.), f);

          // dark insertions
          a = fbm(vec2(15. * a, 10. * r * pulsing));
          f = smoothstep(.4, .9,  a);
          col *= 1. - darkness * f;

          // vignette
          col *= 1. - vignette * smoothstep(.6, .8, r * (.9 + .1 * pulsing));

          // pupil
          f = 1. - smoothstep(.2, .25, r * pulsing);
          col = mix(col, vec3(.0), f);

          // crop
          f = smoothstep(.79, 0.85, r);
          col = mix(col, vec3(1.), f);

          gl_FragColor = vec4(col, 1.);
      }
    </script>

    <script id="vertexShader" type="x-shader/x-vertex">
      varying vec2 vUv;

      void main() {
          vUv = uv;
          vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
          gl_Position = projectionMatrix * mvPosition;
      }
    </script>
    <script>
      // -------------------------------
      // -------------------------------

      // Interactive eyeball animation based on the famous shader created by Inigo Quilez.

      // -------------------------------

      let visualization, pointer, controls, animations;
      const container = document.querySelector(".container");

      let eyeConfig = {
        zoomLevel: 600,
        zoomLevelBounds: [300, 1000],
        shrink: 0,
        fstBaseColor: "#03565c",
        scdBaseColor: "#42cf44",
        midColor: "#f2aa00",
        vignette: 0.65,
        brightness: 0.6,
        darkness: 0.5,
      };

      document.addEventListener("DOMContentLoaded", () => {
        controls = new Controls();
        pointer = new Pointer();
        visualization = new Visualization();
        animations = new Animations();

        pointer.updateCenter();
        visualization.updateSize();

        controls.addShaderControls();
        controls.addSceneControls();

        window.addEventListener("resize", () => visualization.updateSize());
        visualization.loop();

        container
          .querySelector("canvas")
          .addEventListener("click", (e) => pointer.onClick(e));
        document.addEventListener("mousedown", (e) => pointer.onMouseDown(e));
        document.addEventListener("mouseup", (e) => pointer.onMouseUp(e));
        document.addEventListener("mousemove", (e) => pointer.onMouseMove(e));
        document.addEventListener("touchmove", (e) => pointer.onTouchMove(e));
        document.addEventListener("touchend", (e) => pointer.onTouchEnd(e));
        document.addEventListener("touchstart", (e) => pointer.onTouchStart(e));

        container.addEventListener("wheel", (e) => {
          eyeConfig.zoomLevel += 0.1 * e.deltaY;
          eyeConfig.zoomLevel = Math.min(
            eyeConfig.zoomLevel,
            eyeConfig.zoomLevelBounds[1]
          );
          eyeConfig.zoomLevel = Math.max(
            eyeConfig.zoomLevel,
            eyeConfig.zoomLevelBounds[0]
          );
          visualization.setCameraPosition(eyeConfig.zoomLevel);
        });

        animations.eyeAppear.play(0);
        gsap.delayedCall(1.1, () => {
          animations.playShrink.play(0);
        });
      });

      class Pointer {
        constructor() {
          this.mouse = { x: 0, y: 0 };
          this.deltaMove = { x: 0, y: 0 };
          this.previousMousePosition = { x: 0, y: 0 };
          this.pressed = false;
          this.touchMode = false;
          this.dragging = false;
          this.deltaRotationQuaternion = new THREE.Quaternion();
        }
        updateCenter() {
          this.windowHalf = {
            x: window.innerWidth / 2,
            y: window.innerHeight / 2,
          };
        }
        onMouseDown() {
          this.pressed = true;
        }
        onMouseUp() {
          this.pressed = false;
          this.deltaRotationQuaternion.setFromEuler(
            new THREE.Euler(0, 0, 0, "XYZ")
          );
        }
        onMouseMove(e) {
          this.mouse = {
            x: (e.clientX - this.windowHalf.x) / this.windowHalf.x,
            y: (e.clientY - this.windowHalf.y) / this.windowHalf.y,
          };
          this.deltaMove = {
            x: e.offsetX - this.previousMousePosition.x,
            y: e.offsetY - this.previousMousePosition.y,
          };
          this.previousMousePosition = {
            x: e.offsetX,
            y: e.offsetY,
          };
          if (this.pressed) {
            this.deltaRotationQuaternion.setFromEuler(
              new THREE.Euler(
                this.deltaMove.y * (Math.PI / 180),
                this.deltaMove.x * (Math.PI / 180),
                0,
                "XYZ"
              )
            );
            this.dragging = true;
          }
        }
        onTouchMove(e) {
          this.touchMode = true;
          setTimeout(() => (this.dragging = true), 100);
          if (e.touches.length === 1) {
            this.deltaMove = {
              x: e.touches[0].pageX - this.previousMousePosition.x,
              y: e.touches[0].pageY - this.previousMousePosition.y,
            };
            this.previousMousePosition = {
              x: e.touches[0].pageX,
              y: e.touches[0].pageY,
            };
            this.deltaRotationQuaternion.setFromEuler(
              new THREE.Euler(
                this.deltaMove.y * 0.5 * (Math.PI / 180),
                this.deltaMove.x * 0.5 * (Math.PI / 180),
                0,
                "XYZ"
              )
            );
          }
        }
        onTouchStart(e) {
          this.previousMousePosition = {
            x: e.touches[0].pageX,
            y: e.touches[0].pageY,
          };
        }
        onTouchEnd() {
          this.deltaRotationQuaternion.setFromEuler(
            new THREE.Euler(0, 0, 0, "XYZ")
          );
        }
        onClick() {
          if (!this.dragging) {
            animations.playShrink.play(0);
          }
          this.dragging = false;
        }
      }

      class Controls {
        constructor() {
          this.zoomControl = document.querySelector("#zoom-range");

          this.mouseOverControls = false;
          this.gui = new dat.gui.GUI();
          const controls = document.querySelector(".dg.main");
          let timeout;
          controls.addEventListener(
            "mouseenter",
            () => {
              if (!pointer.touchMode)
                timeout = setTimeout(
                  () => (this.mouseOverControls = true),
                  400
                );
            },
            false
          );
          controls.addEventListener(
            "mouseleave",
            () => {
              clearTimeout(timeout);
              this.mouseOverControls = false;
            },
            false
          );
        }
        addShaderControls() {
          this.shrinkContol = this.gui
            .add(
              visualization.eyeShaderMaterial.uniforms.shrink,
              "value",
              -0.9,
              0.3,
              0.05
            )
            .name("shrink");
          this.gui
            .addColor({ color: eyeConfig.fstBaseColor }, "color")
            .onChange((v) => {
              visualization.eyeShaderMaterial.uniforms.base_color_1.value =
                new THREE.Color(v);
            })
            .name("base color #1");
          this.gui
            .addColor({ color: eyeConfig.scdBaseColor }, "color")
            .onChange((v) => {
              visualization.eyeShaderMaterial.uniforms.base_color_2.value =
                new THREE.Color(v);
            })
            .name("base color #2");
          this.gui
            .addColor({ color: eyeConfig.midColor }, "color")
            .onChange((v) => {
              visualization.eyeShaderMaterial.uniforms.mid_color.value =
                new THREE.Color(v);
            })
            .name("mid color");
          this.gui
            .add(
              visualization.eyeShaderMaterial.uniforms.vignette,
              "value",
              0,
              1,
              0.05
            )
            .name("vignette");
          this.gui
            .add(
              visualization.eyeShaderMaterial.uniforms.brightness,
              "value",
              0.2,
              0.65,
              0.05
            )
            .name("brightness");
          this.gui
            .add(
              visualization.eyeShaderMaterial.uniforms.darkness,
              "value",
              0,
              1,
              0.05
            )
            .name("darkness");
        }
        addSceneControls() {
          this.zoomControl.min = eyeConfig.zoomLevelBounds[0];
          this.zoomControl.max = eyeConfig.zoomLevelBounds[1];
          this.zoomControl.value = eyeConfig.zoomLevel;
          this.zoomControl.addEventListener(
            "change",
            () => {
              visualization.setCameraPosition(+this.zoomControl.value);
              this.mouseOverControls = false;
            },
            false
          );
          this.zoomControl.addEventListener(
            "input",
            () => {
              visualization.setCameraPosition(+this.zoomControl.value);
              this.mouseOverControls = true;
            },
            false
          );
        }
      }

      class Animations {
        constructor() {
          this.playShrink = gsap
            .timeline({
              paused: true,
              onUpdate: () => {
                controls.shrinkContol.setValue(
                  visualization.eyeShaderMaterial.uniforms.shrink.value
                );
              },
            })
            .to(visualization.eyeShaderMaterial.uniforms.shrink, {
              duration: 0.5,
              value: -0.9,
              ease: "power2.out",
            })
            .to(visualization.eyeShaderMaterial.uniforms.shrink, {
              duration: 0.7,
              value: 0,
              ease: "power2.inOut",
            });

          this.eyeAppear = gsap
            .timeline({
              paused: true,
            })
            .from(visualization.eyeGroup.position, {
              duration: 2,
              y: 1000,
              ease: "power4.out",
            })
            .from(
              visualization.eyeGroup.rotation,
              {
                duration: 2,
                x: 25,
                z: 5,
                ease: "power3.out",
              },
              0
            )
            .from(
              visualization.shadowMesh.scale,
              {
                duration: 2,
                x: 2,
              },
              0
            );
        }
      }

      class Visualization {
        constructor() {
          this.renderer = new THREE.WebGLRenderer({ antialias: true });
          this.renderer.setPixelRatio(window.devicePixelRatio);
          container.appendChild(this.renderer.domElement);
          this.scene = new THREE.Scene();
          this.eyeGroup = new THREE.Group();
          this.eyeRadius = 30;
          this.camera = new THREE.PerspectiveCamera(
            20,
            window.innerWidth / window.innerHeight,
            1,
            10000
          );

          this.setResponsiveValues();
          this.setupScene();
          this.createEyeball();
          this.createShadow();
          this.render();
        }

        setupScene() {
          this.scene.background = new THREE.Color(0xffffff);
          this.setCameraPosition(eyeConfig.zoomLevel);

          const ambientLight = new THREE.AmbientLight(0x999999, 0.7);
          this.scene.add(ambientLight);

          const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
          directionalLight.position.set(-1, 1, 1);
          this.scene.add(directionalLight);
        }

        setResponsiveValues() {
          this.sceneVerticalOffset = window.innerWidth > 800 ? 0 : 25;
        }

        setCameraPosition(cp) {
          this.camera.position.z = cp;
          this.camera.position.y = this.sceneVerticalOffset;
          controls.zoomControl.value = eyeConfig.zoomLevel;
          eyeConfig.zoomLevel = cp;
        }

        createEyeball() {
          const eyeBallTexture = new THREE.TextureLoader().load(
            "https://ksenia-k.com/img/eyeball.jpg"
          );
          const eyeAddonGeometry = new THREE.SphereGeometry(
            this.eyeRadius,
            128,
            128
          );
          const eyeAddonMaterial = new THREE.MeshPhongMaterial({
            color: 0xffffff,
            emissive: 0x220000,
            opacity: 0.25,
            shininess: 100,
            transparent: true,
            map: eyeBallTexture,
          });
          const eyeAddon = new THREE.Mesh(eyeAddonGeometry, eyeAddonMaterial);
          this.eyeGroup.add(eyeAddon);

          const eyeGeometry = new THREE.SphereGeometry(
            this.eyeRadius - 0.3,
            256,
            256
          );
          this.eyeShaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
              shrink: {
                type: "f",
                value: eyeConfig.shrink,
              },
              base_color_1: {
                type: "v3",
                value: new THREE.Color(eyeConfig.fstBaseColor),
              },
              base_color_2: {
                type: "v3",
                value: new THREE.Color(eyeConfig.scdBaseColor),
              },
              mid_color: {
                type: "v3",
                value: new THREE.Color(eyeConfig.midColor),
              },
              vignette: {
                type: "f",
                value: eyeConfig.vignette,
              },
              brightness: {
                type: "f",
                value: eyeConfig.brightness,
              },
              darkness: {
                type: "f",
                value: eyeConfig.darkness,
              },
            },
            vertexShader: document.getElementById("vertexShader").textContent,
            fragmentShader:
              document.getElementById("fragmentShader").textContent,
          });
          const eye = new THREE.Mesh(eyeGeometry, this.eyeShaderMaterial);
          eye.rotation.y = -Math.PI / 2;
          this.eyeGroup.add(eye);

          this.scene.add(this.eyeGroup);
        }

        createShadow() {
          const canvas = document.createElement("canvas");
          const canvasSize =
            (canvas.width =
            canvas.height =
              this.eyeRadius * 2.5);
          const context = canvas.getContext("2d");
          const gradient = context.createRadialGradient(
            canvasSize * 0.5,
            canvasSize * 0.5,
            0,
            canvasSize * 0.5,
            canvasSize * 0.5,
            canvasSize * 0.4
          );
          gradient.addColorStop(0.2, "rgba(210,210,210,1)");
          gradient.addColorStop(1, "rgba(255,255,255,1)");
          context.fillStyle = gradient;
          context.fillRect(0, 0, canvas.width, canvas.height);
          const shadowTexture = new THREE.CanvasTexture(canvas);
          const shadowMaterial = new THREE.MeshBasicMaterial({
            map: shadowTexture,
          });
          const shadowGeo = new THREE.PlaneBufferGeometry(
            canvasSize,
            canvasSize,
            1,
            1
          );
          this.shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial);
          this.shadowMesh.position.y = -2 * this.eyeRadius;
          this.shadowMesh.rotation.x = -Math.PI / 2.05;
          this.scene.add(this.shadowMesh);
        }

        render() {
          const rotationSpeed = 0.2;
          if (!controls.mouseOverControls) {
            if (!pointer.pressed && !pointer.touchMode) {
              this.eyeGroup.rotation.x +=
                (pointer.mouse.y * 0.3 - this.eyeGroup.rotation.x) *
                rotationSpeed;
              this.eyeGroup.rotation.y +=
                (pointer.mouse.x * 0.6 - this.eyeGroup.rotation.y) *
                rotationSpeed;
            } else {
              this.eyeGroup.quaternion.multiplyQuaternions(
                pointer.deltaRotationQuaternion,
                this.eyeGroup.quaternion
              );
            }
          } else {
            this.eyeGroup.rotation.x +=
              -this.eyeGroup.rotation.x * rotationSpeed * 0.1;
            this.eyeGroup.rotation.y +=
              -this.eyeGroup.rotation.y * rotationSpeed * 0.1;
          }
          this.renderer.render(this.scene, this.camera);
        }

        loop() {
          this.render();
          requestAnimationFrame(this.loop.bind(this));
        }

        updateSize() {
          this.setResponsiveValues();
          this.camera.aspect = window.innerWidth / window.innerHeight;
          this.camera.updateProjectionMatrix();
          this.camera.position.y = this.sceneVerticalOffset;
          this.renderer.setSize(window.innerWidth, window.innerHeight);
        }
      }
    </script>
  </body>
</html>
